Skip to content

GNU Stow


GNU Stow keeps your configuration files in one organized folder, while every application still finds them in their usual place.

It does this with symlinks [small files that point to another file or directory]. The real file lives in your repository. A link in the normal location points to it.

The typical use is dotfiles [config files whose names start with a dot], such as ~/.gitconfig, ~/.zshrc, ~/.config/nvim/, and ~/.config/wezterm/. Instead of leaving them scattered around your home directory, you keep them all in one Git repository, usually ~/dotfiles/.

Six GNU Stow scenarios: the package-is-a-mini-$HOME rule, setting up a new machine, removing and relinking, conflicts with existing files, two packages sharing ~/.config, and ignore lists

The basic idea

Your home directory normally looks like this:

$HOME/
├── .gitconfig
├── .zshrc
└── .config/
    ├── nvim/
    │   └── init.lua
    └── wezterm/
        └── wezterm.lua

You want the real files in Git instead:

~/dotfiles/
├── git/
│   └── .gitconfig
├── zsh/
│   └── .zshrc
├── nvim/
│   └── .config/
│       └── nvim/
│           └── init.lua
└── wezterm/
    └── .config/
        └── wezterm/
            └── wezterm.lua

Stow then creates the links:

~/.gitconfig             →  ~/dotfiles/git/.gitconfig
~/.config/nvim/init.lua  →  ~/dotfiles/nvim/.config/nvim/init.lua

Neovim still opens ~/.config/nvim/init.lua. The file it actually reads sits in ~/dotfiles/, under version control.

Three words you need

Stow directory — the folder that holds your packages, for example ~/dotfiles/.

Package — one top-level folder inside the stow directory. One package per application is the usual choice:

~/dotfiles/
├── git/        ← package
├── zsh/        ← package
├── nvim/       ← package
└── wezterm/    ← package

Target directory — where the links appear, normally $HOME.

You rarely name the target. Stow defaults it to the parent of the stow directory, and the parent of ~/dotfiles is ~. That is why the standard layout works with no extra flags.

The one rule that explains everything

Each package is a miniature copy of the tree you want under $HOME.

Neovim expects ~/.config/nvim/init.lua, so the package holds:

full path in the repo   ~/dotfiles/nvim/.config/nvim/init.lua
package root            ~/dotfiles/nvim/
what is left            .config/nvim/init.lua
where Stow links it     $HOME/.config/nvim/init.lua

Strip the ~/dotfiles/nvim/ prefix and .config/nvim/init.lua is left. Stow reproduces exactly that path under $HOME, as links.

Install

# Ubuntu
sudo apt install stow

# macOS
brew install stow

stow --version

Create your dotfiles repository

mkdir ~/dotfiles
cd ~/dotfiles
git init

mkdir git
mv ~/.gitconfig ~/dotfiles/git/.gitconfig

You now have:

~/dotfiles/
└── git/
    └── .gitconfig

Stow your first package

cd ~/dotfiles
stow git

ls -l ~/.gitconfig
# .gitconfig -> dotfiles/git/.gitconfig

Nested configuration directories

Neovim keeps its config in ~/.config/nvim/, so rebuild that path inside the package:

mkdir -p ~/dotfiles/nvim/.config/nvim
mv ~/.config/nvim/* ~/dotfiles/nvim/.config/nvim/

cd ~/dotfiles
stow nvim

The package looks like this:

~/dotfiles/
└── nvim/
    └── .config/
        └── nvim/
            ├── init.lua
            └── lua/

And the config shows up again at ~/.config/nvim/.

Stow several packages at once

stow git zsh nvim wezterm tmux

Preview before you change anything

The safest habit in Stow. -n means change nothing, -v means tell me what you would do:

stow -n -v nvim     # preview
stow -n -vv nvim    # more detail
stow nvim           # run it for real

Verbosity goes up to -vvvvv, but level 2 is usually enough.

Remove and restow

stow -D nvim    # or --delete: remove the links this package created
stow -R nvim    # or --restow: delete, then stow again

-D never touches ~/dotfiles/nvim/. It only removes the symlinks. Your real files stay in the repository.

Use -R after you move files around inside a package, so stale links get cleaned up.

Naming the source and target explicitly

stow -d ~/dotfiles -t ~ nvim

-d is the stow directory, -t is the target. Same result as cd ~/dotfiles && stow nvim, but it does not depend on your current directory, which makes it the right form inside scripts.

When the target file already exists

If ~/.zshrc exists as a real file and the package also has one, Stow reports a conflict and stops. It does not overwrite your file.

The plain fix is to move the file in yourself:

mv ~/.zshrc ~/dotfiles/zsh/.zshrc
stow zsh

--adopt

--adopt does that move for you: an existing plain file at the target is moved into the package, then linked back.

stow --adopt zsh
git diff

Careful: this overwrites the version in your repository with whatever was sitting in $HOME. Always run it in a clean Git tree and read the diff afterwards. Do not point it at a large tree until you know exactly what it will pull in.

Ignoring files

Some files in a package should never be linked into $HOME: caches, state databases, editor backups.

Stow picks one ignore list, in this order:

  1. .stow-local-ignore in the top level of the package being stowed.
  2. ~/.stow-global-ignore, if the package has no local file.
  3. Its own built-in default list, if neither exists.

They do not stack. A package-local file replaces the global one for that package.

The built-in default already ignores .git, .gitignore, .gitmodules, .svn, emacs backup files, and top-level README*, LICENSE*, COPYING. So a normal repository layout needs no ignore file at all: ~/dotfiles/.git and ~/dotfiles/README.md are not packages, so they are never stowed in the first place.

Per-package ignore files

Each package can have its own, and this is the intended way to do it:

~/dotfiles/
├── nvim/
│   ├── .stow-local-ignore
│   └── .config/
└── zsh/
    ├── .stow-local-ignore
    └── .zshrc

Example ~/dotfiles/nvim/.stow-local-ignore:

# Comments and blank lines are allowed
cache
\.luarc\.json
^/\.config/nvim/lazy-lock\.json

The patterns are Perl regular expressions [text patterns that match names], not shell globs, so a literal dot has to be escaped as \..

Two matching rules, and the difference matters:

  • A pattern without / is matched against the basename only — the file's own name, no directory part. cache ignores every cache anywhere in the package.
  • A pattern with / is matched against the path relative to the package root, starting with /. ^/\.config/nvim/lazy-lock\.json ignores that one exact file.

Matching is anchored at both ends, so cache does not match cache.db. Use cache.* for that.

.stow-local-ignore itself is always ignored, so it never ends up linked into $HOME.

Ignoring from the command line

stow nvim --ignore='cache'

stow myapp \
  --ignore='cache' \
  --ignore='state\.db'

Good for a one-off. The catch is that the rule lives in the command, not in the repository, so it has to go into a script or you will forget it. Prefer .stow-local-ignore for anything permanent.

Directory folding

Stow prefers one link over many. Instead of linking each file:

~/.config/nvim/init.lua  →  ...
~/.config/nvim/lua/      →  ...

it may link the whole directory at once:

~/.config/nvim  →  ~/dotfiles/nvim/.config/nvim

This is called tree folding, and it is normal. Stow unfolds automatically when a second package needs to put something in the same directory.

If a program dislikes having its config directory be a symlink, disable it with --no-folding, which forces one link per file.

Two packages can share .config

~/dotfiles/
├── nvim/
│   └── .config/
│       └── nvim/
└── wezterm/
    └── .config/
        └── wezterm/

Both need ~/.config/. That is fine:

stow nvim
stow wezterm

Result:

~/.config/
├── nvim/     →  ~/dotfiles/nvim/.config/nvim
└── wezterm/  →  ~/dotfiles/wezterm/.config/wezterm

This is the reason to use Stow instead of ln -s ~/dotfiles/.config ~/.config. One big link for .config ties every application together, and you can no longer add, remove, or move one of them on its own.

Optional: --dotfiles

A repository full of hidden folders is awkward to browse. With --dotfiles, Stow renames any dot- prefix to a real dot when it creates the link:

~/dotfiles/zsh/dot-zshrc  →  stowed as  ~/.zshrc
stow --dotfiles zsh

Useful, but pick one convention and keep it. Mixing dot-zshrc and .zshrc in the same repository gets confusing fast, and you must pass the flag every time.

A practical dotfiles structure

~/dotfiles/
├── .gitignore
├── README.md
├── git/
│   └── .gitconfig
├── zsh/
│   ├── .zshrc
│   └── .zprofile
├── nvim/
│   ├── .stow-local-ignore
│   └── .config/
│       └── nvim/
├── wezterm/
│   └── .config/
│       └── wezterm/
├── tmux/
│   └── .tmux.conf
├── starship/
│   └── .config/
│       └── starship.toml
└── scripts/
    └── .local/
        └── bin/
cd ~/dotfiles
stow git zsh nvim wezterm tmux starship scripts

Adding a new application

Say you want to manage Lazygit, whose config is at ~/.config/lazygit/config.yml:

mkdir -p ~/dotfiles/lazygit/.config/lazygit
mv ~/.config/lazygit/config.yml ~/dotfiles/lazygit/.config/lazygit/

cd ~/dotfiles
stow -n -v lazygit    # check the preview first
stow lazygit

git add lazygit
git commit -m "Add lazygit configuration"

That is the whole daily loop: rebuild the path, move the file, preview, stow, commit.

Setup script for a new machine

#!/usr/bin/env bash
set -euo pipefail

DOTFILES="$HOME/dotfiles"

stow \
  --dir="$DOTFILES" \
  --target="$HOME" \
  git \
  zsh \
  nvim \
  wezterm \
  tmux \
  starship

Save it as ~/dotfiles/install.sh, then:

chmod +x ~/dotfiles/install.sh
~/dotfiles/install.sh

Commands worth remembering

stow nvim                      # install one package
stow git zsh nvim              # install several
stow -n -v nvim                # preview, change nothing
stow -D nvim                   # remove its links
stow -R nvim                   # remove and re-create its links
stow -d ~/dotfiles -t ~ nvim   # explicit source and target
stow nvim --ignore='cache'     # skip matching files
stow --adopt nvim              # pull existing $HOME files into the package

The mental model

Do not think of Stow as a complicated symlink command. Think of it this way:

Each package is a small copy of $HOME. Stow makes that small copy show up inside the real $HOME.

~/dotfiles/nvim/          becomes        $HOME/
└── .config/                             └── .config/
    └── nvim/                                └── nvim/
        └── init.lua                             └── init.lua

The real file stays at ~/dotfiles/nvim/.config/nvim/init.lua. The application sees it at ~/.config/nvim/init.lua.

Where to start

~/dotfiles/
├── git/
├── zsh/
├── nvim/
├── wezterm/
└── tmux/
  • One package per application.
  • Preview with stow -n -v PACKAGE before every change.
  • Add a .stow-local-ignore only to the packages that actually need one.
  • Commit after each package you add.

Glossary

  • Dotfiles — configuration files whose names begin with ., such as .gitconfig.
  • Symlink — a small file that points to another file or directory.
  • Stow directory — the folder holding your packages, usually ~/dotfiles.
  • Package — one top-level folder inside the stow directory, managed as a unit.
  • Target directory — where the links appear, usually $HOME. Defaults to the parent of the stow directory.
  • Regular expression — a text pattern used to match names.
  • Basename — a file's own name without any directory in front of it.
  • Tree folding — Stow using one directory symlink instead of many file symlinks.